/*******************************************************************************
 * Copyright 2009 Dop Sun
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
/**
 * Place holder for copyright, needs to be replaced before release.
 */
package com.softao.ming.gde.internal.core.bootstrap;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.softao.ming.gde.internal.core.natures.GdePluginFile;

/**
 * Generates the bootstrap application.
 * <p>
 * It creates the following details:
 * <ul>
 * <li>Source Folder Structure
 * <li>Module File
 * <li>Public HTML File
 * <li>Entry Point Class: including the registering the plugin.
 * </ul>
 */
public class BootstrapGenerator {
	/**
	 * Generates the bootstrap application to start the GWT product.
	 * 
	 * @param args
	 *            arguments for generating the bootstrap application.
	 * @param monitor
	 * @return <code>true</code> if the bootstrap application generated
	 *         successfully; otherwise, <code>false</code>.
	 */
	public boolean generate(BootstrapGeneratorArgs args, IProgressMonitor monitor) {
		monitor.subTask("Generating the bootstrap application...");

		if (!createPublic(args, monitor)) {
			return false;
		}
		monitor.worked(1);
		if (monitor.isCanceled()) {
			return false;
		}

		if (!createBootstrapModule(args, monitor)) {
			return false;
		}
		monitor.worked(1);
		if (monitor.isCanceled()) {
			return false;
		}

		if (!createEntryPoint(args, monitor)) {
			return false;
		}
		monitor.worked(1);
		if (monitor.isCanceled()) {
			return false;
		}

		return true;
	}

	private boolean createPublic(BootstrapGeneratorArgs args, IProgressMonitor monitor) {
		BootstrapProduct product;
		try {
			product = new BootstrapProduct(args);
		} catch (CoreException e) {
			e.printStackTrace();
			return false;
		}
		File publicDir = new File(args.getLocation() + "/com/softao/ming/gwt/bootstrap/public");
		publicDir.mkdirs();
		File defaultHtml = new File(publicDir.toString() + "/default.html");
		
		StringBuilder sb = new StringBuilder();
		sb.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n");
		sb.append("<html>\n");
		sb.append("    <head>\n");
		sb.append("        <title>" + product.getName() + "</title>\n");
		sb.append("        <script language='javascript' src='com.softao.ming.gwt.bootstrap.MingBootstrap.nocache.js'></script>\n");	
		sb.append("    </head>\n");
		sb.append("    <body>\n");
		sb.append("        <iframe src=\"javascript:''\" id=\"__gwt_historyFrame\" style=\"width:0;height:0;border:0\"></iframe>\n");
		sb.append("    </body>\n");
		sb.append("</html>");
		
		try {
			writeFile(defaultHtml, sb);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

	private boolean createBootstrapModule(BootstrapGeneratorArgs args, IProgressMonitor monitor) {
		File publicDir = new File(args.getLocation() + "/com/softao/ming/gwt/bootstrap");
		publicDir.mkdirs();
		File moduleFile = new File(publicDir.toString() + "/MingBootstrap.gwt.xml");
		
		StringBuilder sb = new StringBuilder();
		sb.append("<module>\n");
		sb.append("    <!-- Inherit GWT Standard Module -->\n");
		sb.append("    <inherits name='com.google.gwt.user.User'/>\n");
		sb.append("\n");
		sb.append("    <!-- Specify the application entry point class -->\n");
		sb.append("    <entry-point class='com.softao.ming.gwt.bootstrap.client.BootstrapApplication'/>\n");
		sb.append("\n");
		sb.append("    <!-- Processing plugin module -->\n");
		
		// For each plugin, search for the module, and inherits all of them.
		for (IPluginModelBase plugin : PluginRegistry.getActiveModels()) {
			ZipFile zipFile = null;
			try {
				String installLocation = plugin.getInstallLocation();
				File file = new File(installLocation);
				if (file.isFile()) {
					zipFile = new ZipFile(file, ZipFile.OPEN_READ);
					printModulesInJar(sb, zipFile);
					zipFile.close();
				} else {
					printModulesInFolder(sb, file);
				}
			} catch (IOException e) {
				
			}
		}
		
		sb.append("</module>");
		
		try {
			writeFile(moduleFile, sb);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

	private boolean createEntryPoint(BootstrapGeneratorArgs args, IProgressMonitor monitor) {
		BootstrapProduct product;
		try {
			product = new BootstrapProduct(args);
		} catch (CoreException e) {
			e.printStackTrace();
			return false;
		}
		
		File publicDir = new File(args.getLocation() + "/com/softao/ming/gwt/bootstrap/client");
		publicDir.mkdirs();
		File entryPointFile = new File(publicDir.toString() + "/BootstrapApplication.java");
		
		StringBuilder sb = new StringBuilder();
		sb.append("package com.softao.ming.gwt.bootstrap.client;\n");
		sb.append("\n");
		sb.append("import com.google.gwt.core.client.EntryPoint;\n");
		sb.append("import com.softao.ming.gwt.core.internal.runtime.client.ExtensionRegistry;\n");
		sb.append("import com.softao.ming.gwt.core.internal.runtime.client.ObjectFactory;\n");
		sb.append("import com.softao.ming.gwt.core.runtime.client.application.IObjectFactory;\n");
		sb.append("import com.softao.ming.gwt.core.runtime.client.application.Platform;\n");
		sb.append("\n");
		sb.append("public class BootstrapApplication implements EntryPoint {\n");
		sb.append("\n");
		sb.append("    public void onModuleLoad() {\n");
		sb.append("        registerPlugin();\n");
		sb.append("        registerObjectFactory();\n");
		sb.append("        " + product.getApplication().getClassName() + " application = new " + product.getApplication().getClassName() + "();\n");
		sb.append("        application.start();\n");
		sb.append("    }\n");
		sb.append("\n");
		sb.append("    private void registerPlugin() {\n");
		sb.append("        ExtensionRegistry registry = (ExtensionRegistry)Platform.getExtensionRegistry();\n");
		
		List<String> pluginClasses = new ArrayList<String>();
		// For each plugin, register.
		for (IPluginModelBase plugin : PluginRegistry.getActiveModels()) {
			ZipFile zipFile = null;
			try {
				String installLocation = plugin.getInstallLocation();
				File file = new File(installLocation);
				if (file.isFile()) {
					zipFile = new ZipFile(file, ZipFile.OPEN_READ);
					registerPluginInJar(sb, zipFile, pluginClasses);
					zipFile.close();
				} else {
					registerPluginInFolder(sb, file, pluginClasses);
				}
			} catch (IOException e) {
				e.printStackTrace();
				continue;
			}
		}
		
		sb.append("    }\n");
		sb.append("\n");
		
		sb.append("    private void registerObjectFactory() {\n");
		sb.append("        ObjectFactory objectFactory = (ObjectFactory)Platform.getObjectFactory();\n");
		sb.append("        objectFactory.merge(new IObjectFactory() {\n");
		sb.append("            public <T> T create(Class<T> type, String className) {\n");
		for (String type : pluginClasses) {
			sb.append("                if (\"" + type + "\".equals(className)) \n");
			sb.append("                    return (T)new " + type + "();\n");
		}
		sb.append("                return null;\n");
		sb.append("            }\n");
		sb.append("\n");
		sb.append("            public String[] getClassNames() {\n");
		sb.append("                return new String[] {\n");
		for (String type : pluginClasses) {
			sb.append("                \"" + type + "\",\n");
		}
		sb.append("                };\n");
		sb.append("            }\n");
		sb.append("        });\n");
		sb.append("    }\n");
		sb.append("\n");
		
		sb.append("}");
		
		try {
			writeFile(entryPointFile, sb);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}
	
	private void registerPluginInFolder(StringBuilder sb, File baseLocation, List<String> pluginClasses) throws IOException {
		File gwtPluginFile = new File(baseLocation, GdePluginFile.FILE_NAME);
		if (!gwtPluginFile.exists()) {
			return;
		}
		File pluginXmlFile = new File(baseLocation, "plugin.xml");
		if (!pluginXmlFile.exists()) {
			return;
		}
		
		InputStream pluginStream = new FileInputStream(pluginXmlFile);
		registerPluginStream(sb, pluginStream, pluginClasses);
		pluginStream.close();
	}

	private void registerPluginInJar(StringBuilder sb, ZipFile zipFile, List<String> pluginClasses) throws IOException {
		if (zipFile.getEntry(GdePluginFile.FILE_NAME) == null) {
			return;
		}
		ZipEntry pluginEntry = zipFile.getEntry("plugin.xml");
		if (pluginEntry != null) {
			InputStream inputStream = zipFile.getInputStream(pluginEntry);
			registerPluginStream(sb, inputStream, pluginClasses);
			inputStream.close();
		}
	}
	
	private void registerPluginStream(StringBuilder sb, InputStream inputStream, List<String> pluginClasses) throws IOException {
		InputStreamReader isr = new InputStreamReader(inputStream);
		BufferedReader reader = new BufferedReader(isr);
		
		StringBuilder sbPlugin = new StringBuilder();
		
		String line = reader.readLine();
		while (line != null) {
			sbPlugin.append(line);
			sbPlugin.append("\n");
			
			line = reader.readLine();
		}
		
		String pluginXml = sbPlugin.toString();
		resolvePluginClasses(pluginXml, pluginClasses);
		
		pluginXml = pluginXml.replace("\"", "\\\"").replace("\n", "\"  +\n\"");
		sb.append("        registry.registerPlugin(\"" + pluginXml + "\");\n");
		
		
	}
	
	private void resolvePluginClasses(String pluginXml, List<String> pluginClasses) throws IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document document = builder.parse(new InputSource(new StringReader(pluginXml)));
			resolvePluginClasses(document.getDocumentElement(), pluginClasses);
		} catch (ParserConfigurationException e) {
			throw new IOException("An internal operation failed.", e);
		} catch (SAXException e) {
			throw new IOException("An internal operation failed.", e);
		}

	}
	
	private void resolvePluginClasses(Element element, List<String> pluginClasses) {
		NamedNodeMap attributes = element.getAttributes();
		for (int i = 0; i < attributes.getLength(); i++) {
			Node item = attributes.item(i);
			if (item.getNodeName().equals("class")) {
				String className = item.getNodeValue();
				
				if (!pluginClasses.contains(className)) {
					pluginClasses.add(className);
				}
			}
		}
		
		NodeList childNodes = element.getChildNodes();
		for (int i = 0; i < childNodes.getLength(); i++) {
			Node node = childNodes.item(i);
			if (node instanceof Element) {
				resolvePluginClasses((Element)node, pluginClasses);
			}
		}
	}
	
	private void printModulesInJar(StringBuilder sb, ZipFile zipFile) {
		if (zipFile.getEntry(GdePluginFile.FILE_NAME) == null) {
			return;
		}
		ZipEntry buildEntry = zipFile.getEntry("build.properties");
		if (buildEntry == null) {
			return;
		}
		
		String[] sourceFolders;
		try {
			InputStream buildEntryStream = zipFile.getInputStream(buildEntry);
			sourceFolders = getSourceFolders(buildEntryStream);
			buildEntryStream.close();
			if (sourceFolders.length == 0) {
				return;
			}
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}

		Enumeration<? extends ZipEntry> entries = zipFile.entries();
		while (entries.hasMoreElements()) {
			ZipEntry zipEntry = entries.nextElement();
			if (zipEntry.isDirectory()) {
				continue;
			}
			
			String entryName = zipEntry.getName();
			if (entryName.endsWith(".gwt.xml")) {
				for (String sourceFolder : sourceFolders) {
					if (entryName.startsWith(sourceFolder)) {
						entryName = entryName.substring(sourceFolder.length() + 1);
						entryName = entryName.substring(0, entryName.length() - ".gwt.xml".length());
						entryName = entryName.replace('/', '.').replace('\\', '.');
						sb.append("    <inherits name='" + entryName + "'/>\n");
					}
				}
			}
		}
	}
	
	private void printModulesInFolder(StringBuilder sb, File baseLocation) {
		File gwtPluginFile = new File(baseLocation, GdePluginFile.FILE_NAME);
		if (!gwtPluginFile.exists()) {
			return;
		}
		File buildFile = new File(baseLocation, "build.properties");
		if (!buildFile.exists()) {
			return;
		}
		
		String[] sourceFolders;
		try {
			InputStream buildEntryStream = new FileInputStream(buildFile);
			sourceFolders = getSourceFolders(buildEntryStream);
			buildEntryStream.close();
			if (sourceFolders.length == 0) {
				return;
			}
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}

		for (String sourceFolder : sourceFolders) {
			File sourceFile = new File(baseLocation + "/" + sourceFolder);
			printModulesInFolder(sb, sourceFile, "");
		}
	}

	private static void printModulesInFolder(StringBuilder sb, File parent, String path) {
		for (File file : parent.listFiles()) {
			if (file.isDirectory()) {
				if (path.length() == 0) {
					printModulesInFolder(sb, file, file.getName());
				} else {
					printModulesInFolder(sb, file, path + "." + file.getName());
				}
			} else {
				String fileName = file.getName();
				if (fileName.endsWith(".gwt.xml")) {
					String moduleName = path.length() == 0 ? fileName : path + "." + fileName;
					moduleName = moduleName.substring(0, moduleName.length() - ".gwt.xml".length());
					sb.append("    <inherits name='" + moduleName + "'/>\n");
				}
			}
		}
	}
	
	/**
	 * @param buildEntryStream
	 * @return source
	 */
	public static String[] getSourceFolders(InputStream buildEntryStream) {
		Properties prop = new Properties();
		try {
			prop.load(buildEntryStream);
		} catch (IOException e) {
			e.printStackTrace();
			return new String[0];
		}
		String source = prop.getProperty("source..");
		return source == null ? new String[0] : source.split(",");
	}
		
	private static void writeFile(File file, StringBuilder sb) throws IOException {
		FileOutputStream fos = new FileOutputStream(file);
		OutputStreamWriter osw = new OutputStreamWriter(fos);
		BufferedWriter writer = new BufferedWriter(osw);
		writer.append(sb.toString());
		writer.flush();
		fos.close();
	}

}
